package se.devoteam.nexus.ldap;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;

import org.codehaus.plexus.component.annotations.Component;
import org.codehaus.plexus.component.annotations.Requirement;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException;
import org.jsecurity.authc.AuthenticationInfo;
import org.jsecurity.authc.AuthenticationToken;
import org.jsecurity.authc.SimpleAuthenticationInfo;
import org.jsecurity.authc.UsernamePasswordToken;
import org.jsecurity.authz.AuthorizationInfo;
import org.jsecurity.realm.Realm;
import org.jsecurity.realm.ldap.AbstractLdapRealm;
import org.jsecurity.realm.ldap.LdapContextFactory;
import org.jsecurity.realm.ldap.LdapUtils;
import org.jsecurity.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonatype.jsecurity.locators.users.PlexusUserLocator;

/**
 * LDAP authenticating realm for Sonatype Nexus.
 * 
 * @author Anders Hammar
 */
@Component(role = Realm.class, hint = "LdapAuthenticatingRealm")
public class LdapAuthenticatingRealm extends AbstractLdapRealm implements Initializable {

	private final Logger logger = LoggerFactory.getLogger(LdapAuthenticatingRealm.class);

	@Requirement
	private LdapConfigurationManager configuration;

	@Requirement
	private LdapContextFactory ctxFactory;

	@Requirement(role = PlexusUserLocator.class, hint = "LDAP")
	private LdapUserLocator userLocator;

	@Override
	public String getName() {
		return LdapAuthenticatingRealm.class.getName();
	}

	protected Logger getLogger() {
		return logger;
	}

	public void initialize() throws InitializationException {
		// Use our LdapContextFactory implementation
		super.setLdapContextFactory(this.ctxFactory);

		super.init();

		// Test the LDAP connection
		LdapContext ctx = null;
		try {
			getLogger().debug("About to test LDAP connection");
			ctx = this.ctxFactory.getSystemLdapContext();
			getLogger().debug("LDAP connection configuration seems to be ok");
		} catch (NamingException e) {
			getLogger().error("LDAP connection configuration not working; check configuration or service", e);
			throw new InitializationException("LDAP connection configuration not working");
		} finally {
			LdapUtils.closeContext(ctx);
			ctx = null;
		}
		getLogger().info("LdapAuthenticatingRealm initialized");
	}

	/**
	 * Builds an {@link AuthenticationInfo} object by querying the LDAP context for the specified
	 * username. This method first retrieves the user's DN by searching the LDAP context using the
	 * provided username, then binds to the LDAP server using that DN and password - which, if
	 * successful, indicates that the password is correct.
	 * 
	 * @param token the authentication token provided by the user
	 * @param ldapContextFactory the factory used to build connections to the LDAP server
	 * @return an AuthenticationInfo instance containing information retrieved from LDAP
	 * @throws NamingException if any LDAP errors occur during the search
	 */
	@Override
	protected AuthenticationInfo queryForAuthenticationInfo(AuthenticationToken token, LdapContextFactory ldapContextFactory)
			throws NamingException {
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		String username = upToken.getUsername();
		if (username == null) {
			getLogger().debug("Username is null - failing ldap authentication");
			return null;
		}

		getLogger().debug("Authenticating user '{}' through LDAP", username);

		LdapContext ctx = null;
		// First, retrieve the user's DN
		String userDn = null;
		try {
			// LDAP search filter
			String searchFilterExpr = this.configuration.getSearchUserFilterExpr();
			Object[] searchFilterArgs = {username};
			getLogger().debug("LDAP user search filter: {}", searchFilterExpr);

			// Attributes to return
			String userIdNameAttrId = this.configuration.getUserIdAttribute();
			String searchAttributes[] = {userIdNameAttrId};
			
			// Search for user object
			ctx = this.ctxFactory.getSystemLdapContext();
			NamingEnumeration<SearchResult> answers =
				this.userLocator.search(ctx, searchFilterExpr, searchFilterArgs, searchAttributes);

			if (answers.hasMore()) { // user found
				getLogger().debug("User object found");
				SearchResult tmpAnswer = answers.next();
				userDn = tmpAnswer.getNameInNamespace();
			} else {
				getLogger().debug("User object not found - failing ldap authentication");
				return null;
			}
			
			if (answers.hasMore()) { // username not unique
				getLogger().error("User ID not unique in LDAP context - failing ldap authentication");
				return null;
			}
		} finally {
			LdapUtils.closeContext(ctx);
			ctx = null;
		}

		// Then authenticate by binding using the retrieved DN
		try {
			getLogger().debug("LDAP authentication principal: {}", userDn);
			// Bind using the user's DN (principal) and password provided by the user
			ctx = ldapContextFactory.getLdapContext(userDn, String.valueOf(upToken.getPassword()));
		} finally {
			LdapUtils.closeContext(ctx);
		}

		getLogger().debug("User authenticated through LDAP");
		return buildAuthenticationInfo(upToken.getUsername(), upToken.getPassword());
	}

	protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
		return new SimpleAuthenticationInfo(username, password, getName());
	}

	/**
	 * No authorization taking place (always returns <code>null</code>).
	 * 
	 * @return <code>null</code>
	 */
	@Override
	protected AuthorizationInfo queryForAuthorizationInfo(PrincipalCollection principals, LdapContextFactory ldapContextFactory)
			throws NamingException {
		return null;
	}
}
